feat: migrate HTTP transport to stateless mode, upgrade go-sdk to v1.7.0 - #162
feat: migrate HTTP transport to stateless mode, upgrade go-sdk to v1.7.0#162iavael wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the MCP HTTP transport from stateful (session/SSE-oriented) behavior to stateless JSON request/response mode, enabled via the go-sdk v1.7.0 StreamableHTTPOptions.Stateless support.
Changes:
- Upgrade
github.com/modelcontextprotocol/go-sdkfrom v1.6.1 to v1.7.0. - Switch the HTTP handler to stateless + JSON responses, and remove the now-inapplicable
--mcp.session-timeoutflag. - Re-enable HTTP server
WriteTimeout(previously disabled for SSE-style streaming).
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates the Docker example wording to reflect stateless JSON HTTP transport. |
| pkg/mcp/server.go | Configures the go-sdk streamable HTTP handler for stateless application/json request/response behavior. |
| cmd/prometheus-mcp/main.go | Removes session-timeout flag usage, updates handler wiring, and adjusts HTTP server timeout behavior. |
| go.mod | Bumps go-sdk dependency to v1.7.0. |
| go.sum | Updates checksums for the go-sdk version bump. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
cmd/prometheus-mcp/main.go:357
- PR description says the HTTP server WriteTimeout is being "restored to 30s", but the code now sets it to
prometheus.timeout + 5s(default 1m5s). Please align the implementation with the stated behavior (set WriteTimeout back to 30s) or update the PR description to reflect the new timeout semantics.
// request/response cycles. Stateless HTTP transport uses
// application/json responses that complete in a single round
// trip, so standard timeouts apply.
ReadTimeout: 30 * time.Second,
WriteTimeout: *flagPrometheusTimeout + (5 * time.Second),
d86cfd8 to
291d0e1
Compare
Signed-off-by: Iavael <905853+iavael@users.noreply.github.com>
291d0e1 to
9a5640c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
cmd/prometheus-mcp/main.go:357
initHTTPServersetsWriteTimeoutto*flagPrometheusTimeout(the Prometheus backend client timeout, default 1m), but the PR description says the HTTP server write timeout is being restored to 30s for stateless JSON request/response. Reusing the Prometheus backend timeout here also couples unrelated concerns and makes the actual HTTP server behavior surprising.
WriteTimeout: *flagPrometheusTimeout,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
cmd/prometheus-mcp/main.go:138
- The
--mcp.keepalive-intervalhelp text still talks about “connected MCP sessions” and “idle connections from dropping”, which is confusing now that HTTP is stateless and stdio isn’t a network connection. Reword this flag description to describe keepalives in terms of the client/transport and dead-peer detection.
flagMcpKeepaliveInterval = kingpin.Flag(
"mcp.keepalive-interval",
"Interval for sending keepalive pings to connected MCP sessions."+
" If the peer fails to respond, the session is closed."+
" Most useful for stdio transport to prevent idle connections from dropping.",
).Default("30s").Duration()
pkg/mcp/server.go:246
NewStreamableHTTPHandlernow depends on go-sdk stateless behavior (Stateless: true+JSONResponse: true), but there’s no test asserting the HTTP handler actually responds with JSON (vs SSE framing) and stays compatible across go-sdk upgrades. Adding a focused httptest-based unit test for the/mcphandler (e.g., responseContent-Type: application/jsonand no session semantics) would help prevent regressions.
handler := mcp.NewStreamableHTTPHandler(
func(r *http.Request) *mcp.Server {
return server
},
&mcp.StreamableHTTPOptions{
Stateless: true,
JSONResponse: true,
Logger: logger,
},
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
pkg/mcp/server.go:207
KeepAlivesupport appears to be removed entirely (the SDKServerOptionsno longer setsKeepAlive). While this makes sense for stateless HTTP, it also disables keepalive pings for thestdiotransport, which is still long-lived/streaming. If the intent was to drop keepalive only for HTTP, consider keepingKeepAliveconfigurable (and applying it at least forstdio), or update the docs/PR description to explicitly call out that keepalive was removed for all transports.
&mcp.ServerOptions{
Instructions: instrx,
Logger: logger.WithGroup("go_sdk_logger"),
Capabilities: caps,
},
|
@copilot is keepalive really necessary for stdio transport? |
|
@tjhop can you, please, give your review? |
tjhop
left a comment
There was a problem hiding this comment.
Thanks for getting this PR so fast after the official release! The approach overall is solid and the first-order mechanics of things that need changing in order to do the migration (like the unwiring the keepalive/timeout) are all sound. Thankfully, the transition to stateless is overall pretty easy/transparent to consumers. However, there are some (I think) inaccuracies in some of the implementation vs the literal spec. There are also second-order changes and implications that need to be fixed/addressed. I've left feedback inline where possible, but some of the second-order changes mentioned aren't in the changeset to comment on, so listing here:
- this will break some telemetry. right now, the server ready metric gets set during the
initializemethod, but the new "stateless" version of the spec doesn't have aninitialize, so the server ready metric goes dark - we should expand telemetry hooks in middleware.go to capture
server/discovermethod, I think. it's not required for clients to use it, so it shouldn't indicate server readiness, though - the server ready metric will need to be re-homed. it didn't really live in the right place before anyway, as the initialize method required a client to connect, so the server would start up successfully and the metric would indicate "not ready" until the first client connected and completed an initialize. really, the server is "ready" once it's about to kick off serving MCP requests. so I would think that would mean:
- when running an stdio server, setting as ready directly before starting the stdio server
- when running an http server, setting as ready directly after adding the
/mcphandler to the webserver, since a failure there would bubble up through through to oklog/run and kill the program - unset server ready during cleanup
Thanks for the help taking this on! Lemme know if you have any questions
| flagMcpKeepaliveInterval = kingpin.Flag( | ||
| "mcp.keepalive-interval", | ||
| "Interval for sending keepalive pings to connected MCP sessions."+ | ||
| " If the peer fails to respond, the session is closed."+ | ||
| " Most useful for HTTP transports to prevent idle connections from dropping.", | ||
| ).Default("30s").Duration() | ||
|
|
||
| flagMcpSessionTimeout = kingpin.Flag( | ||
| "mcp.session-timeout", | ||
| "Idle session timeout for HTTP transport MCP sessions.", | ||
| ).Default("10m").Duration() | ||
|
|
There was a problem hiding this comment.
Removing flags is user-facing and can cause unexpected breakages in other ways (ie, the binary bombing at starting when provided with unexpected flags that suddenly don't work).
We should keep the flags for now, turn them into no-ops, and deprecate them. Let's leave the flag declarations here and update the help descriptions/README with the deprecation warning. we can still continue to unwire the timeout/keepalive configs
| SessionTimeout: sessionTimeout, | ||
| Logger: logger, | ||
| Stateless: true, | ||
| JSONResponse: true, |
There was a problem hiding this comment.
JSONResponse is independent of Stateless, and not required in order for it to work.
In fact, enabling it actually silently breaks client notification logging -- this switches response away from text/event-stream, which breaks the ability to send the notifications mid-stream/call.
| // trip, so standard timeouts apply. | ||
| ReadTimeout: 30 * time.Second, | ||
| WriteTimeout: 0, | ||
| WriteTimeout: *flagPrometheusTimeout, |
There was a problem hiding this comment.
Let's keep the WriteTimeout: 0, we need it disabled to continue to support stream responses.
We should also probably bump IdleTimeout: 120 * time.Second as well -- this should reduce HTTP connection churn for normal clients that are potentially above that idle timeout, such as longer prometheus scrape intervals/LB health checks. The latter of which is arguably more consequential, as I'm realizing now this probably also means potentially sporadic 502s.
| // request/response cycles. Stateless HTTP transport uses | ||
| // application/json responses that complete in a single round | ||
| // trip, so standard timeouts apply. |
There was a problem hiding this comment.
I don't think this is accurate to the spec, it does not require JSON and responses do not need to be a single round trip.
References:
- Server chooses JSON or SSE per response
- client MUST support both
- some endpoints are explicitly long lived streams:
- https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5f5440bb26a62e2cf3440b92da5a667efa03b267/docs/specification/2026-07-28/basic/transports/streamable-http.mdx?plain=1#L127-L135
- https://github.com/modelcontextprotocol/modelcontextprotocol/blob/5f5440bb26a62e2cf3440b92da5a667efa03b267/docs/specification/2026-07-28/basic/patterns/subscriptions.mdx?plain=1#L7-L10
- sse keepalive notes
there's other bits in the spec relevant to this, but I think this makes the point
| // NewStreamableHTTPHandler creates an HTTP handler for the MCP server using | ||
| // stateless HTTP transport. In stateless mode, each request is handled | ||
| // independently without session tracking, and responses are returned as | ||
| // application/json rather than text/event-stream. |
There was a problem hiding this comment.
this comment is also inaccurate
|
|
||
| ```shell | ||
| # Streamable HTTP transport (capable of SSE as well) | ||
| # Stateless HTTP transport (application/json request/response) |
There was a problem hiding this comment.
this should be reduced to a s/Streamable/Stateless/
Switches the HTTP MCP transport from stateful (SSE, session-tracked) to stateless mode: each request is an independent JSON round-trip with no session ID validation or server-initiated messages.
Changes
go-sdkv1.6.1 → v1.7.0 —Statelessfield onStreamableHTTPOptionswas introduced in v1.7.0NewStreamableHTTPHandler— setsStateless: trueandJSONResponse: true; drops thesessionTimeoutandKeepAliveparameters (inapplicable in stateless mode)--mcp.session-timeoutflag removed — sessions no longer exist; flag would be a no-opWriteTimeoutmatches prometheus timeout — previously forced to0to accommodate never-finishing SSE streams; stateless JSON responses complete within a single round trip--mcp.keepalive-intervalflag removed — network connection is no longer streamable, flag would be a no-op